package org.mayanjun.code.dalint.mybatis.dao;

import org.apache.ibatis.session.SqlSession;
import org.mayanjun.code.dalint.Sharding;
import org.mayanjun.code.dalint.ShardingEntityAccessor;
import org.mayanjun.code.dalint.id.IdWorker;
import org.mayanjun.code.dalint.mybatis.DataBaseRouter;
import org.mayanjun.code.dalint.mybatis.DynamicMapper;
import org.mayanjun.code.dalint.mybatis.PreparedQueryParser;
import org.mayanjun.code.dalint.parser.QueryParser;
import org.mayanjun.code.dalint.parser.SQLParameter;
import org.mayanjun.code.dalmeta.entity.PersistableEntity;
import org.mayanjun.code.dalmeta.query.Query;
import org.mayanjun.code.dalmeta.query.QueryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

/**
 * StandardDAO
 *
 * @author mayanjun(6/24/16)
 */
public class DynamicDAO implements DataBaseRouteDAO, ShardingEntityAccessor, InitializingBean {

    private static final Logger LOG = LoggerFactory.getLogger(DynamicDAO.class);

    private DataBaseRouter router;
    private IdWorker idWorker;
    private Map<Class<?>, Class<?>> entityMapperClasses = new IdentityHashMap<Class<?>, Class<?>>();

    private String[] mapperPackages;
    private QueryParser parser = new PreparedQueryParser(DynamicMapper.PARAM_NAME);
    private Sharding defaultSharding = new DefaultSharding();

    private boolean useMasterDataBaseIfNotFound = true;

    public void setUseMasterDataBaseIfNotFound(boolean useMasterDataBaseIfNotFound) {
        this.useMasterDataBaseIfNotFound = useMasterDataBaseIfNotFound;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(router, "router must be set");
        scanMapperPackages();
    }

    private void scanMapperPackages() throws Exception {
        Assert.isTrue(this.mapperPackages != null && this.mapperPackages.length > 0, "mapperPackages can not be empty");
        for (String mapperPackage : this.mapperPackages) {
            scanMapperPackage(mapperPackage);
        }
    }

    private void scanMapperPackage(String mapperPackage) throws Exception {
        String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(mapperPackage) + "/**/*.class";
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resolver);
        Resource resources[] = resolver.getResources(pattern);

        TypeFilter filter = new AssignableTypeFilter(DynamicMapper.class);

        if (resources != null && resources.length > 0) {
            for (Resource r : resources) {
                MetadataReader reader = readerFactory.getMetadataReader(r);
                boolean matched = filter.match(reader, readerFactory);
                if (matched) {
                    String className = reader.getClassMetadata().getClassName();
                    Class<?> cls;
                    try {
                        cls = Class.forName(className);
                        Type ts[] = cls.getGenericInterfaces();
                        if (ts != null) {
                            for (Type t : ts) {
                                ParameterizedType pt = (ParameterizedType) t;
                                if (pt.getRawType() == DynamicMapper.class) {
                                    Type ats[] = pt.getActualTypeArguments();
                                    Class<?> beanType = (Class<?>) ats[0];
                                    entityMapperClasses.put(beanType, cls);
                                    LOG.info("Mapper found: " + beanType + " --> " + cls);
                                    break;
                                }
                            }
                        }
                    } catch (ClassNotFoundException e) {
                        LOG.error("Mapper class not found: " + className);
                    }
                }
            }
        }
    }

    @Override
    public <T extends PersistableEntity> List<T> query(Query<T> query) {
        return query(query, null);
    }

    @Override
    public <T extends PersistableEntity> T queryOne(Query<T> query) {
        return queryOne(query, defaultSharding);
    }

    @Override
    public int update(PersistableEntity bean) {
        return update(bean, defaultSharding);
    }

    @Override
    public int update(PersistableEntity bean, Query<? extends PersistableEntity> query) {
        return update(bean, defaultSharding, query);
    }

    @Override
    public long save(PersistableEntity bean) {
        return save(bean, defaultSharding);
    }

    @Override
    public int delete(PersistableEntity bean) {
        return delete(bean, defaultSharding);
    }

    @Override
    public int delete(Query<? extends PersistableEntity> query) {
        return delete(query, defaultSharding);
    }

    @Override
    public <T extends PersistableEntity> T getExclude(PersistableEntity bean, String... excludeFields) {
        return getExclude(bean, null, excludeFields);
    }

    @Override
    public <T extends PersistableEntity> T getInclude(PersistableEntity bean, String... includeFields) {
        return getInclude(bean, null, includeFields);
    }

    @Override
    public <T extends PersistableEntity> List<T> query(Query<T> query, Sharding sharding) {
        SQLParameter<T> parameter = parser.parse(query);
        SqlSession sqlSession = getSqlSession(sharding, parameter);
        DynamicMapper<T> mapper = (DynamicMapper<T>) getMapper(query.getBeanType(), sqlSession);
        return mapper.query(parameter, sharding);
    }

    @Override
    public <T extends PersistableEntity> T queryOne(Query<T> query, Sharding sharding) {
        if(query.getLimit() > 1) query.setLimit(1);
        List<T> list = query(query, sharding);
        if(list != null && !list.isEmpty()) {
            return list.get(0);
        }
        return null;
    }

    @Override
    public int update(PersistableEntity bean, Sharding sharding) {
        bean.setCreatedTime(null);
        bean.setModifiedTime(new Date());

        SqlSession sqlSession = getSqlSession(sharding, bean);

        DynamicMapper<PersistableEntity> mapper = getMapper(bean.getClass(), sqlSession);
        return mapper.update(bean, sharding);
    }

    @Override
    public int update(PersistableEntity bean, Sharding sharding, Query<? extends PersistableEntity> query) {
        bean.setCreatedTime(null);
        bean.setModifiedTime(new Date());

        SQLParameter<PersistableEntity> parameter = (SQLParameter<PersistableEntity>)parser.parse(query);
        parameter.setEntity(bean);
        SqlSession sqlSession = getSqlSession(sharding, bean);
        DynamicMapper<PersistableEntity> mapper = getMapper(bean.getClass(), sqlSession);
        return mapper.queryUpdate(parameter, sharding);
    }

    @Override
    public long save(PersistableEntity bean, Sharding sharding) {
        Date now = new Date();
        if(bean.getCreatedTime() == null) bean.setCreatedTime(now);
        if(bean.getModifiedTime() == null) bean.setModifiedTime(now);

        Long id = null;
        try {
            id = (Long) bean.getId();
        } catch (Exception e){}

        if(id == null) {
            id = this.idWorker.nextId();
            bean.setId(id);
        }

        SqlSession sqlSession = getSqlSession(sharding, bean);
        DynamicMapper<PersistableEntity> mapper = getMapper(bean.getClass(), sqlSession);

        int ret = mapper.insert(bean, sharding);
        if(ret <= 0) return 0;
        return id;
    }

    @Override
    public int delete(PersistableEntity bean, Sharding sharding) {
        SqlSession sqlSession = getSqlSession(sharding, bean);
        DynamicMapper<PersistableEntity> mapper = getMapper(bean.getClass(), sqlSession);
        return mapper.delete(bean, sharding);
    }

    @Override
    public int delete(Query<? extends PersistableEntity> query, Sharding sharding) {
        Class<? extends PersistableEntity> beanType = query.getBeanType();
        SQLParameter<PersistableEntity> parameter = (SQLParameter<PersistableEntity>)parser.parse(query);
        SqlSession sqlSession = getSqlSession(sharding, parameter);
        DynamicMapper<PersistableEntity> mapper = getMapper(beanType, sqlSession);
        return mapper.queryDelete(parameter, sharding);
    }

    @Override
    public <T extends PersistableEntity> T getExclude(PersistableEntity bean, Sharding sharding, String... excludeFields) {
        Query<T> query = QueryBuilder.custom((Class<T>) bean.getClass()).andEquivalent("id", bean.getId()).excludeFields(excludeFields).limit(1).build();
        List<T> list = this.query(query, sharding);
        if (list != null && list.size() > 0) return list.get(0);
        return null;
    }

    @Override
    public <T extends PersistableEntity> T getInclude(PersistableEntity bean, Sharding sharding, String... includeFields) {
        Query<T> query = QueryBuilder.custom((Class<T>) bean.getClass()).andEquivalent("id", bean.getId()).includeFields(includeFields).limit(1).build();
        List<T> list = this.query(query, sharding);
        if (list != null && list.size() > 0) return list.get(0);
        return null;
    }

    private SqlSession getSqlSession(Sharding sharding, Object source) {
        //SqlSession sqlSession = getDataBaseRouter().getMasterDataBaseSqlSession(); // forced to use MASTER DB
        SqlSession sqlSession = getDataBaseRouter().getDataBaseSqlSession(sharding, source);
        if(sqlSession != null) return sqlSession;

        if(useMasterDataBaseIfNotFound) sqlSession = getDataBaseRouter().getMasterDataBaseSqlSession();
        Assert.notNull(sqlSession, "Can not get SqlSession");

        return sqlSession;
    }

    public DynamicMapper<PersistableEntity> getMapper(Class<?> beanType, SqlSession sqlSession) {
        try {
            Class<?> mapperClass = entityMapperClasses.get(beanType);
            Assert.notNull(mapperClass, "Mapper class not found");
            DynamicMapper<PersistableEntity> mapper = (DynamicMapper<PersistableEntity>) sqlSession.getMapper(mapperClass);
            return mapper;
        } catch (Throwable e) {
            String message = "No mapper interface found for bean type " + beanType;
            RuntimeException exception = new RuntimeException(message, e);
            LOG.error(message, exception);
            throw exception;
        }
    }

    @Override
    public DataBaseRouter getDataBaseRouter() {
        return router;
    }

    public IdWorker getIdWorkerBroker() {
        return idWorker;
    }

    @Required
    public void setIdWorkerBroker(IdWorker idWorkerBroker) {
        Assert.notNull(idWorkerBroker, "IdWorkerBroker can not be null");
        this.idWorker = idWorkerBroker;
    }

    public String[] getMapperPackages() {
        return mapperPackages;
    }

    @Required
    public void setMapperPackages(String mapperPackages[]) {
        this.mapperPackages = mapperPackages;
    }

    public DataBaseRouter getRouter() {
        return router;
    }

    @Required
    public void setRouter(DataBaseRouter router) {
        this.router = router;
    }

    private static class DefaultSharding implements Sharding {

        @Override
        public String getDataBaseName(Object source) {
            return null;
        }

        @Override
        public String getTableName(Object source) {
            return null;
        }

        @Override
        public Map<String, Set<String>> getDataBaseNames(Object source) {
            return null;
        }

    }
}
